Skip to content

fix(spec-parity): render the six Tier-1 spec values right instead of silently wrong (#2941) - #2993

Merged
os-zhuang merged 1 commit into
mainfrom
claude/tier1-spec-silent-output-1a1721
Jul 30, 2026
Merged

fix(spec-parity): render the six Tier-1 spec values right instead of silently wrong (#2941)#2993
os-zhuang merged 1 commit into
mainfrom
claude/tier1-spec-silent-output-1a1721

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2941 (Tier 1 of the #2901 audit — spec values that validate and then render silently wrong output; audit doc).

Every finding below produced output that looked correct and wasn't. Each fix lands with a spec-parity guard per the #2897 template (summary-spec-parity.test.ts) plus behavior tests, so the pair fails the moment either side moves again.

The six fixes

Finding Before Now
Pivot aggregation fell back to sum (PivotTable.tsx) count_distinct / array_agg / string_agg (engine-level names with no client renderer) returned a sum — a plausible wrong total, no signal Out-of-vocabulary aggregations refuse loudly with a role="alert" notice; the implemented set is pinned to the spec's 5-name ChartAggregateFunctionSchema (the UI-side subset the spec carved out for exactly this reason). The default: branch now returns NaN as a tripwire, unreachable behind the gate
Report chart fell back to bar (DatasetReportRenderer.tsx) 10 of 19 ChartTypeSchema values silently drew a bar chart planReportChart classifies all 19: the 12 series families reach the generic chart verbatim (horizontal-bar stays horizontal, funnel/treemap/sankey draw their real shapes — the renderer grew them since the audit); the 5 single-value families (gauge/solid-gauge/metric/kpi/bullet) render the measure as a number from a dimensionless dataset query (server-aggregated; the spec's own comment calls them "honest single-value variants pending a real dial renderer"); table/pivot add no duplicate chart (the grouped table beneath is that rendering); out-of-spec values get a visible notice instead of a guessed bar
selection.type: 'single' rendered multi-select (data-table.tsxObjectGrid.tsx) selectable was consumed as a bare truthy: per-row checkboxes and select-all, single never distinguished single is a real mode: replace-on-select (never two rows), no select-all header, and ObjectGrid stops offering the cross-page "select all N matching" escalation. Legacy true keeps meaning multi-select
Filter type: 'select' rendered multi-check (UserFilters.tsx) A single-choice filter accepted many values The spec names both select and multi-select, which settles the semantic question: select is single-choice. It renders radios, picking replaces (badge × clears), and restored/default multi-value selections clamp to one. Authored types only — an omitted type is inferred from the field and keeps the historical multi-check UX
addRecord.position: 'both' collapsed to top (ListView.tsx) Binary ternary; the bottom button never rendered resolveAddRecordPlacement maps all three spec values; both renders both buttons. (Discovered adjacent, left alone: the renderer's implicit default is top while the spec's declared default is bottom — moving stored views' button is a UX change beyond this PR; noted in the resolver's doc comment)
tabular vs summary resolved from data, not type (DatasetReportRenderer.tsx) Identical branch; the difference was whether report.rows happened to be non-empty resolveReportPresentation makes the DECLARED type pick the branch. summary ("grouped by row") now carries a server-computed grand-total footer — totals: { groupings: [[]] }, same machinery as the matrix path, never recombined client-side (ADR-0021 red line), older servers degrade to no footer. tabular ("simple list") is the same selection as a plain list with no totals. Joined blocks get the same per-block dispatch

Semantic decisions the issue asked for

  • selection.type: 'single' — replace-on-select radio semantics and a hard cap at one (they coincide: selecting row B deselects row A; unchecking clears). The spec's single | multiple contrast pair plus every mainstream grid's behavior settle this.
  • filter type: 'select' vs 'multi-select' — the enum contains both names, so select must mean the thing multi-select isn't: one value at a time.

Guard prerequisite

components, plugin-dashboard, plugin-report gain the @objectstack/spec devDependency (the issue's noted prerequisite; plugin-list/plugin-grid already had it). components also pins zod@^4.4.3 — without it, shadcn's MCP SDK leaks zod@3 into the importer context and pnpm forks a second store instance of the spec (spec@17.0.0-rc.0(ai@7.0.37(zod@3.25.76))), the exact two-instances failure mode the audit's method note warns about. The lockfile now resolves one variant repo-wide.

Verification

  • New guards: 5 parity suites + behavior tests (39 new tests), all green.
  • Full suites of the five touched packages: plugin-report + plugin-dashboard 211 ✓, plugin-grid 310 ✓, plugin-list 232 ✓, components 288 ✓ (+4 files that hit the known-heavy import('../renderers') 30 s hook timeout under a contended machine; all 73 tests pass re-run in isolation).
  • tsc --noEmit clean on all five packages; eslint 0 errors on every touched file.

Refs #2901. Tier 2 (#2942), Tier 3 (#2943), forks (#2944) and vocabulary consolidation (#2945) stay separate.

🤖 Generated with Claude Code

…silently wrong (#2941)

Every row here validated at authoring time and produced output that looked
correct and wasn't (#2901 Tier 1 — worse than a blank cell, because nobody
asks why a plausible number is wrong):

- PivotTable: an out-of-vocabulary aggregation (`count_distinct`,
  `array_agg`, `string_agg` — engine names with no client renderer) returned
  a SUM through the `default:` branch. It now refuses loudly with a visible
  notice; the implemented set is pinned to the spec's 5-name
  `ChartAggregateFunctionSchema`.
- Report chart: 10 of 19 `ChartTypeSchema` values silently drew a bar.
  `planReportChart` now classifies all 19 — the 12 series families reach the
  generic chart verbatim (`horizontal-bar` stays horizontal), the 5
  single-value families render the measure as a server-aggregated number
  (dimensionless dataset query — no client math), `table`/`pivot` add no
  duplicate chart, and out-of-spec values get a visible notice.
- `selection.type: 'single'`: `selectable` was a bare truthy, so single
  rendered the full multi-select UX. The data-table now enforces
  replace-on-select with no select-all header, and ObjectGrid stops offering
  the cross-page "select all N matching" escalation in single mode.
- Filter `type: 'select'`: the spec names both `select` and `multi-select`,
  so `select` is single-choice. It now renders radios and replaces the pick
  (badge × clears); restored/default multi-value selections clamp to one.
  Inferred (omitted) types keep the historical multi-check UX.
- `addRecord.position: 'both'` collapsed to `top` through a binary ternary;
  both buttons now render.
- `tabular` vs `summary` was resolved from whether `rows` was non-empty; the
  DECLARED type now picks the branch: summary (and degraded matrix) carries
  a server-computed grand-total footer (`totals: { groupings: [[]] }`, ADR-0021
  red line respected), tabular is the same selection as a plain list.

Each fix lands with a spec-parity guard per the #2897 template
(`summary-spec-parity.test.ts`) plus behavior tests; `components`,
`plugin-dashboard` and `plugin-report` gain the `@objectstack/spec`
devDependency that makes those guards possible (`components` also pins
`zod@^4` so the spec resolves to the same store instance as the rest of the
repo instead of forking a second `zod@3` peer variant via shadcn's MCP SDK).

Refs #2941, #2901

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 30, 2026 7:48am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-819pZI6R.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 451.02KB 98.25KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 134.67KB 34.24KB
fields (index.js) 221.10KB 54.18KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.93KB 42.67KB
plugin-dashboard (index.js) 110.24KB 28.47KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.52KB 53.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.45KB 25.12KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.53KB 47.06KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.89KB 23.41KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.24KB 10.51KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.47KB 20.82KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.92KB 0.93KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tier 1 (#2901): spec values that render silently WRONG output

1 participant